/*
 * 11/19/04 1.0 moved to LGPL.
 * 
 * 11/17/04 Uncomplete frames discarded. E.B, javalayer@javazoom.net 
 *
 * 12/05/03 ID3v2 tag returned. E.B, javalayer@javazoom.net 
 *
 * 12/12/99 Based on Ibitstream. Exceptions thrown on errors, Temporary removed seek functionality. mdm@techie.com
 *
 * 02/12/99 : Java Conversion by E.B , javalayer@javazoom.net
 *
 * 04/14/97 : Added function prototypes for new syncing and seeking
 * mechanisms. Also made this file portable. Changes made by Jeff Tsay
 *
 *  @(#) ibitstream.h 1.5, last edit: 6/15/94 16:55:34
 *  @(#) Copyright (C) 1993, 1994 Tobias Bading (bading@cs.tu-berlin.de)
 *  @(#) Berlin University of Technology
 *-----------------------------------------------------------------------
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */
package org.mp3transform;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

/**
 * This class is responsible for parsing an MPEG audio bitstream.
 */
public final class Bitstream {
    /**
     * Synchronization control constant for the initial synchronization to the start of a frame.
     */
    static final byte INITIAL_SYNC = 0;
    /**
     * Synchronization control constant for non-initial frame synchronizations.
     */
    static final byte STRICT_SYNC = 1;
    /**
     * Maximum size of the frame buffer. max. 1730 bytes per frame: 144 * 384kbit/s / 32000 Hz + 2 Bytes CRC
     */
    private static final int BUFFER_INT_SIZE = 433;
    /**
     * The frame buffer that holds the data for the current frame.
     */
    private final int[] frameBuffer = new int[BUFFER_INT_SIZE];
    /**
     * Number of valid bytes in the frame buffer.
     */
    private int frameSize;
    /**
     * The bytes read from the stream.
     */
    private final byte[] frameBytes = new byte[BUFFER_INT_SIZE * 4];
    // Index into frameBuffer where the next bits are retrieved.
    private int wordPointer;
    /**
     * Number (0-31, from MSB to LSB) of next bit for getBits()
     */
    private int bitIndex;
    private int syncWord;
    private boolean singleChMode;
    private static final int[] BITMASK = {
            0, // dummy
            0x00000001, 0x00000003, 0x00000007, 0x0000000F, 0x0000001F, 0x0000003F, 0x0000007F, 0x000000FF, 0x000001FF, 0x000003FF, 0x000007FF, 0x00000FFF, 0x00001FFF, 0x00003FFF,
            0x00007FFF, 0x0000FFFF, 0x0001FFFF };
    private final PushbackInputStream source;
    private final Header header = new Header();
    private final byte[] syncBuffer = new byte[4];
    private byte[] rawID3v2 = null;
    private boolean firstFrame = true;

    public Bitstream(InputStream in) {
        source = new PushbackInputStream(in, BUFFER_INT_SIZE * 4);
        loadID3v2();
        closeFrame();
    }

    private void loadID3v2() {
        int size = -1;
        try {
            // read ID3v2 header
            source.mark(10);
            size = readID3v2Header();
        } catch (IOException e) {
            // ignore
        } finally {
            try {
                // unread ID3v2 header
                source.reset();
            } catch (IOException e) {
                // ignore
            }
        }
        // load ID3v2 tags.
        try {
            if (size > 0) {
                rawID3v2 = new byte[size];
                this.readBytes(rawID3v2, 0, rawID3v2.length);
            }
        } catch (IOException e) {
            // ignore
        }
    }

    /**
     * Parse ID3v2 tag header to find out size of ID3v2 frames.
     * 
     * @param in MP3 InputStream
     * @return size of ID3v2 frames + header
     * @throws IOException
     * @author JavaZOOM
     */
    private int readID3v2Header() throws IOException {
        byte[] buff = new byte[4];
        int size = -10;
        readBytes(buff, 0, 3);
        if (buff[0] == 'I' && buff[1] == 'D' && buff[2] == '3') {
            readBytes(buff, 0, 3);
            readBytes(buff, 0, 4);
            size = (buff[0] << 21) + (buff[1] << 14) + (buff[2] << 7) + buff[3];
        }
        return size + 10;
    }

    /**
     * Reads and parses the next frame from the input source.
     * 
     * @return the Header describing details of the frame read, or null if the end of the stream has been reached.
     * @throws IOException
     */
    public Header readFrame() throws IOException {
        try {
            Header result = readNextFrame();
            if (firstFrame) {
                result.parseVBR(frameBytes);
                firstFrame = false;
            }
            return result;
        } catch (EOFException e) {
            return null;
        }
    }

    private Header readNextFrame() throws IOException {
        if (frameSize == -1) {
            while (true) {
                boolean ok = header.readHeader(this);
                if (ok) {
                    break;
                }
                closeFrame();
            }
        }
        return header;
    }

    void unreadFrame() throws IOException {
        if (wordPointer == -1 && bitIndex == -1 && frameSize > 0) {
            source.unread(frameBytes, 0, frameSize);
        }
    }

    public void closeFrame() {
        frameSize = -1;
        wordPointer = -1;
        bitIndex = -1;
    }

    /**
     * Determines if the next 4 bytes of the stream represent a frame header.
     */
    boolean isSyncCurrentPosition(int syncMode) throws IOException {
        int read = readBytes(syncBuffer, 0, 4);
        int headerString = ((syncBuffer[0] << 24) & 0xFF000000) | ((syncBuffer[1] << 16) & 0x00FF0000) | ((syncBuffer[2] << 8) & 0x0000FF00) | ((syncBuffer[3] << 0) & 0x000000FF);
        try {
            source.unread(syncBuffer, 0, read);
        } catch (IOException ex) {
            // ignore
        }
        if (read == 0) {
            return true;
        } else if (read == 4) {
            return isSyncMark(headerString, syncMode, syncWord);
        } else {
            return false;
        }
    }

    /**
     * Get next 32 bits from bitstream. They are stored in the headerstring. syncmod allows Synchro flag ID The returned
     * value is False at the end of stream.
     * 
     * @param syncMode
     */
    int syncHeader(byte syncMode) throws IOException {
        boolean sync;
        int headerString;
        // read additional 2 bytes
        int bytesRead = readBytes(syncBuffer, 0, 3);
        if (bytesRead != 3) {
            throw new EOFException();
        }
        headerString = ((syncBuffer[0] << 16) & 0x00FF0000) | ((syncBuffer[1] << 8) & 0x0000FF00) | ((syncBuffer[2] << 0) & 0x000000FF);
        do {
            headerString <<= 8;
            if (readBytes(syncBuffer, 3, 1) != 1) {
                throw new EOFException();
            }
            headerString |= (syncBuffer[3] & 0x000000FF);
            sync = isSyncMark(headerString, syncMode, syncWord);
        } while (!sync);
        return headerString;
    }

    private boolean isSyncMark(int headerString, int syncMode, int word) {
        boolean sync = false;
        if (syncMode == INITIAL_SYNC) {
            sync = ((headerString & 0xFFE00000) == 0xFFE00000); // SZD: MPEG 2.5
        } else {
            sync = ((headerString & 0xFFF80C00) == word) && (((headerString & 0x000000C0) == 0x000000C0) == singleChMode);
        }
        // filter out invalid sample rate
        if (sync) {
            sync = (((headerString >>> 10) & 3) != 3);
        }
        // filter out invalid layer
        if (sync) {
            sync = (((headerString >>> 17) & 3) != 0);
        }
        // filter out invalid version
        if (sync) {
            sync = (((headerString >>> 19) & 3) != 1);
        }
        return sync;
    }

    /**
     * Reads the data for the next frame. The frame is not parsed until parse frame is called.
     */
    int readFrameData(int byteSize) throws IOException {
        int numread = 0;
        numread = readFully(frameBytes, 0, byteSize);
        frameSize = byteSize;
        wordPointer = -1;
        bitIndex = -1;
        return numread;
    }

    /**
     * Parses the data previously read with readFrameData().
     */
    void parseFrame() {
        // Convert bytes read to int
        int b = 0;
        byte[] byteRead = frameBytes;
        int byteSize = frameSize;
        for (int k = 0; k < byteSize; k = k + 4) {
            byte b0 = 0;
            byte b1 = 0;
            byte b2 = 0;
            byte b3 = 0;
            b0 = byteRead[k];
            if (k + 1 < byteSize) {
                b1 = byteRead[k + 1];
            }
            if (k + 2 < byteSize) {
                b2 = byteRead[k + 2];
            }
            if (k + 3 < byteSize) {
                b3 = byteRead[k + 3];
            }
            frameBuffer[b++] = ((b0 << 24) & 0xFF000000) | ((b1 << 16) & 0x00FF0000) | ((b2 << 8) & 0x0000FF00) | (b3 & 0x000000FF);
        }
        wordPointer = 0;
        bitIndex = 0;
    }

    /**
     * Read bits from buffer into the lower bits of an unsigned int. The LSB contains the latest read bit of the stream.
     * (1 <= numberOfBits <= 16)
     */
    int getBits(int numberOfBits) {
        int returnValue = 0;
        int sum = bitIndex + numberOfBits;
        // TODO There is a problem here, wordpointer could be -1 ?!
        if (wordPointer < 0) {
            System.out.println("wordPointer < 0");
            wordPointer = 0;
        }
        if (sum <= 32) {
            // all bits contained in *wordpointer
            returnValue = (frameBuffer[wordPointer] >>> (32 - sum)) & BITMASK[numberOfBits];
            bitIndex += numberOfBits;
            if (bitIndex == 32) {
                bitIndex = 0;
                wordPointer++;
            }
            return returnValue;
        }
        int right = (frameBuffer[wordPointer] & 0x0000FFFF);
        wordPointer++;
        int left = (frameBuffer[wordPointer] & 0xFFFF0000);
        returnValue = ((right << 16) & 0xFFFF0000) | ((left >>> 16) & 0x0000FFFF);
        returnValue >>>= 48 - sum;
        returnValue &= BITMASK[numberOfBits];
        bitIndex = sum - 32;
        return returnValue;
    }

    /**
     * Set the word we want to sync the header to. In Big-Endian byte order
     */
    void setSyncWord(int s) {
        syncWord = s & 0xFFFFFF3F;
        singleChMode = ((s & 0x000000C0) == 0x000000C0);
    }

    /**
     * Reads the exact number of bytes from the source input stream into a byte array.
     * 
     * @param b The byte array to read the specified number of bytes into.
     * @param offs The index in the array where the first byte read should be stored.
     * @param len the number of bytes to read.
     * 
     * @exception Exception is thrown if the specified number of bytes could not be read from the stream.
     */
    private int readFully(byte[] b, int offs, int len) throws IOException {
        // TODO does not in fact throw an exception, probably return not required
        int read = 0;
        while (len > 0) {
            int bytesRead = source.read(b, offs, len);
            if (bytesRead == -1) {
                while (len-- > 0) {
                    b[offs++] = 0;
                }
                break;
            }
            read = read + bytesRead;
            offs += bytesRead;
            len -= bytesRead;
        }
        return read;
    }

    /**
     * Simlar to readFully, but doesn't throw exception when EOF is reached.
     */
    private int readBytes(byte[] b, int offs, int len) throws IOException {
        int totalBytesRead = 0;
        while (len > 0) {
            int bytesRead = source.read(b, offs, len);
            if (bytesRead == -1) {
                break;
            }
            totalBytesRead += bytesRead;
            offs += bytesRead;
            len -= bytesRead;
        }
        return totalBytesRead;
    }
}
